/**
 * @file filter
 * @author liaoxuezhi@baidu.com
 */
/* eslint fecs-indent: [0, "space", 2, 2] */

import * as React from 'react';
import {
  PureComponent,
  PropTypes
} from 'react';
import {Renderer, render, evalExpression} from '../../renderer';
import * as cx from 'classnames';
import './combo-form';
import * as deepEqual from 'deep-equal';
import * as assign from 'object-assign';
import {setVariable} from '../../util';
import {findDOMNode} from 'react-dom';
import { DragSource, DropTarget, DragDropContext} from 'react-dnd';

const CARD_TYPE = 'combo';
var utils = require('../formsy/utils.js');
var validationRules = require('../formsy/validationRules.js');
var formDataToObject = require('form-data-to-object');
let emptyArray = [];
export class ComboForm extends PureComponent {
  static propTypes = {
    className: PropTypes.string,
    onChange: PropTypes.func,
    onRemove: PropTypes.func,
    removeAble: PropTypes.bool,
    form: PropTypes.object
  };

  static defaultProps = {
    onSuccess: function () {},
    onError: function () {},
    onSubmit: function () {},
    onValidSubmit: function () {},
    onInvalidSubmit: function () {},
    onValid: function () {},
    onInvalid: function () {},
    onChange: function () {},
    validationErrors: null,
    preventExternalInvalidation: false
  };

  defaultValue = {};

  static childContextTypes = {
    formsy: PropTypes.object
  };

  inputs = null;

  constructor(props) {
    super(props);

    this.attachToForm = this.attachToForm.bind(this);
    this.detachFromForm = this.detachFromForm.bind(this);
    this.validate = this.validate.bind(this);
    this.isFormDisabled = this.isFormDisabled.bind(this);
    this._validate = this._validate.bind(this);
    this.setDefaultValue = this.setDefaultValue.bind(this);

    this.state = {
      isValid: true,
      isSubmitting: false,
      canChange: false
    };
  }

  getChildContext() {
    return {
      formsy: {
        attachToForm: this.attachToForm,
        detachFromForm: this.detachFromForm,
        validate: this.validate,
        isFormDisabled: this.isFormDisabled,
        isValidValue: (component, value) => {
          return this.runValidation(component, value).isValid;
        }
      }
    }
  }

  _validate() {
    return this.state.isValid;
  }

  componentWillMount() {
    this.inputs = [];
  }

  componentDidMount() {
    const {
      onChange,
      data
    } = this.props;

    onChange && onChange(assign({}, this.defaultValue, data));
  }

  setDefaultValue(name, value) {
    this.defaultValue = setVariable(name, value, this.defaultValue);
  }

  attachToForm(component) {
    if (this.inputs.indexOf(component) === -1) {
      this.inputs.push(component);
    }

    this.validate(component);
  }

  detachFromForm(component) {
    if (component.props.unsetValueOnInvisible) {
      const modal = this.getModel();
      const name = component.props.name;
      const onChange = this.props.onChange;

      if (name && modal[name] && onChange) {
        modal[name] = null;
        onChange(modal, true);
      }
    }

    var componentPos = this.inputs.indexOf(component);

    if (componentPos !== -1) {
      this.inputs = this.inputs.slice(0, componentPos)
        .concat(this.inputs.slice(componentPos + 1));
    }
  }

  isFormDisabled() {
    return this.props.disabled;
  }

  getCurrentValues() {
    return this.inputs.reduce((data, component) => {
      let name = component.props.name;
      data[name] = component.state._value;
      return data;
    }, {});
  }

  getPristineValues() {
    return this.inputs.reduce((data, component) => {
      var name = component.props.name;
      data[name] = component.props.defaultValue;
      return data;
    }, {});
  }

  isChanged(values) {
    return !deepEqual(values || this.getCurrentValues(), this.getPristineValues())
  }

  renderForm(form) {
    const {$path, data, locals, disabled} = this.props;
    form = assign({}, {
      type:"basic-form",
      mode: form.mode || 'normal',
    }, form);

    return render(`${$path}/basic-form`, form, {
      onInit: this.props.onChange,
      elementsOnly: true,
      setDefaultValue: this.setDefaultValue,
      disabled,
      data,
      locals
    });
  }

  render() {
    const {
      className,
      controls,
      disabled,
      data,
      locals,
      removeAble,
      onRemove,
      $path,
      form,
      children,
      dragable,
      onRequestAction
    } = this.props;

    if (children) {
      return (
        <div className={cx('combo-form', className)}>
          {children}
        </div>
      );
    }


    return !dragable ? (
      <div
        className={cx('combo-form', className)}
      >
        <div className="hbox">
          {controls && controls.map((control, key) => {
            if (control.hiddenOn && evalExpression(control.hiddenOn, data) || control.visibleOn && !evalExpression(control.visibleOn, data)) {
              return null;
            }

            if (control && control.name &&  control.hasOwnProperty('value')) {
              assign(this.defaultValue, {[control.name]: control.value});
            }
            return render(`${$path}/form-item`, control, {
              disabled: control.disabled === undefined ? disabled : control.disabledOn ? evalExpression(control.disabledOn, data) : control.disabled,
              label: control.type == 'button' ? control.label : '',
              desc: '',
              data,
              locals,
              wrappedComponent: 'div',
              wrappedProps: {
                className: cx('col combo-formitem', control.columnClassName)
              },
              key: control.name,
              detectPropName: 'data',
              defaultValue: control.value || '',
              setDefaultValue: this.setDefaultValue,
              onRequestAction,
              value: data && data.hasOwnProperty(control.name) ? data[control.name] : (control.value || '')
            })})
          }
          { form && (
            <div className={'col combo-subform'}>
              {this.renderForm(form)}
            </div>)
          }
          {removeAble && (
            <div className={cx('col text-right', 'v-top')} style={{width: 32}}>
              <button
                type="button"
                onClick={onRemove}
                className="btn btn-default  btn-xs rounded m-t-xs"
                disabled={disabled}
              >
                <i className="fa fa-minus text-danger" />
              </button>
            </div>
          )}
        </div>
      </div>
    ) : (
      <DraggableItem
        className={cx('combo-form', className)}
        id={this.props.index}
        findRow={this.props.findRow}
        moveRow={this.props.moveRow}
        moveRowConfirm={this.props.moveRowConfirm}
      >
        <div className="hbox">
          {controls && controls.map((control, key) => {
            if (control && control.name &&  control.hasOwnProperty('value')) {
              assign(this.defaultValue, {[control.name]: control.value});
            }
            return render(`${$path}/form-item`, control, {
              disabled: control.disabled === undefined ? disabled : control.disabled,
              label: '',
              desc: '',
              data,
              locals,
              wrappedComponent: 'div',
              wrappedProps: {
                className: cx('col combo-formitem', control.columnClassName)
              },
              key,
              detectPropName: 'data',
              defaultValue: control.value || '',
              setDefaultValue: this.setDefaultValue,
              value: data && data.hasOwnProperty(control.name) ? data[control.name] : (control.value || '')
            })})
          }
          { form && (
            <div className={'col combo-subform'}>
              {this.renderForm(form)}
            </div>)
          }
          {removeAble && (
            <div className={cx('col text-right v-middle')} style={{width: 32, lineHeight: '100%'}}>
              <button
                type="button"
                onClick={onRemove}
                className="btn btn-default  btn-xs rounded"
                disabled={disabled}
              >
                <i className="fa fa-minus text-danger" />
              </button>
            </div>
          )}
          <div className="col v-middle drag-bar p-l-sm" style={{width: 32, lineHeight: '100%'}}>
            <i className="fa fa-bars"></i>
          </div>
        </div>
      </DraggableItem>
    );
  }
  submit (event) {

    event && event.preventDefault();
    var model = this.getModel();

    // 为了支持监控 submit 修改。
    let hooks = this.inputs.filter(item => item.input && item.input.onSubmit);
    let prevented = hooks.some(item => item.input.onSubmit(model, item, this) === false);

    // Trigger form as not pristine.
    // If any inputs have not been touched yet this will make them dirty
    // so validation becomes visible (if based on isPristine)
    this.setFormPristine(false);
    this.props.onSubmit(model, this.resetModel, this.updateInputsWithError);
    this.state.isValid && !prevented ? this.props.onValidSubmit(model, this.resetModel, this.updateInputsWithError) : this.props.onInvalidSubmit(model, this.resetModel, this.updateInputsWithError);
    return true;
  }

  mapModel (model) {

    if (this.props.mapping) {
      return this.props.mapping(model)
    } else {
      return formDataToObject.toObj(Object.keys(model).reduce((mappedModel, key) => {

        var keyArray = key.split('.');
        var base = mappedModel;
        while (keyArray.length) {
          var currentKey = keyArray.shift();
          base = (base[currentKey] = keyArray.length ? base[currentKey] || {} : model[key]);
        }

        return mappedModel;

      }, {}));
    }
  }

  getModel () {
    var currentValues = this.getCurrentValues();
    return this.mapModel(currentValues);
  }

  // Reset each key in the model to the original / initial / specified value
  resetModel (data) {
    this.inputs.forEach(component => {
      var name = component.props.name;
      if (data && data.hasOwnProperty(name)) {
        component.setValue(data[name]);
      } else {
        component.resetValue();
      }
    });
    this.validateForm();
  }

  setInputValidationErrors (errors) {
    this.inputs.forEach(component => {
      var name = component.props.name;
      var args = [{
        _isValid: !(name in errors),
        _validationError: typeof errors[name] === 'string' ? [errors[name]] : errors[name]
      }];
      component.setState.apply(component, args);
    });
  }

  updateInputsWithError (errors) {
    Object.keys(errors).forEach((name, index) => {
      var component = utils.find(this.inputs, component => component.props.name === name);
      if (!component) {
        throw new Error('You are trying to update an input that does not exist. ' +
          'Verify errors object with input names. ' + JSON.stringify(errors));
      }
      var args = [{
        _isValid: this.props.preventExternalInvalidation || false,
        _externalError: typeof errors[name] === 'string' ? [errors[name]] : errors[name]
      }];
      component.setState.apply(component, args);
    });
  }

  setFormPristine (isPristine) {
    this.setState({
      _formSubmitted: !isPristine
    });

    this.inputs.forEach((component, index) => {
      component.setState({
        _formSubmitted: !isPristine,
        _isPristine: isPristine
      });
    });
  }

  validate (component) {
    let isChanged = false;

    // Trigger onChange
    if (this.state.canChange) {
      // isChanged = this.isChanged();
      let model = this.getModel();
      //if (!isChanged) {
        let name = component.props.name;
        isChanged = !utils.isSame(model[name], this.props.data && this.props.data[name]);
      //}

      isChanged && this.props.onChange(model);
    }

    var validation = this.runValidation(component);
    // Run through the validations, split them up and call
    // the validator IF there is a value or it is required
    component.setState({
      _isValid: validation.isValid,
      _isRequired: validation.isRequired,
      _validationError: validation.error,
      _externalError: null
    }, () => {

      // 支持修改即提交的功能。
      if (component.props.submitOnChange && this.state.canChange && isChanged) {
        this.lazySubmit();
      } else {
        this.validateForm();
      }

    });

  }

  // Checks validation on current value or a passed value
  runValidation (component, value) {
    var currentValues = this.getCurrentValues();
    var validationErrors = component.props.validationErrors;
    var validationError = component.props.validationError;
    value = arguments.length === 2 ? value : component.state._value;
    var validationResults = this.runRules(value, currentValues, component._validations, component);
    var requiredResults = this.runRules(value, currentValues, component._requiredValidations, component);

    // the component defines an explicit validate function
    if (typeof component.validate === "function") {
      validationResults.failed = component.validate() ? [] : ['failed'];
    }

    var isRequired = Object.keys(component._requiredValidations).length ? !!requiredResults.success.length : false;
    var isValid = !validationResults.failed.length && !(this.props.validationErrors && this.props.validationErrors[component.props.name]);

    return {
      isRequired: isRequired,
      isValid: isRequired ? false : isValid,
      error: (function () {

        if (isValid && !isRequired) {
          return emptyArray;
        }

        if (validationResults.errors.length) {
          return validationResults.errors;
        }

        if (this.props.validationErrors && this.props.validationErrors[component.props.name]) {
          return typeof this.props.validationErrors[component.props.name] === 'string' ? [this.props.validationErrors[component.props.name]] : this.props.validationErrors[component.props.name];
        }

        if (isRequired) {
          var error = validationErrors[requiredResults.success[0]];
          return error ? [error] : null;
        }

        if (validationResults.failed.length) {
          return validationResults.failed.map(function(failed) {
            let msg = validationErrors[failed];
            return msg ? msg.replace(/\$1/g, component._validations && component._validations[failed]) : validationError;
          }).filter(function(x, pos, arr) {
            // Remove duplicates
            return arr.indexOf(x) === pos;
          });
        }

      }.call(this))
    };

  }

  runRules (value, currentValues, validations, component) {
    var results = {
      errors: [],
      failed: [],
      success: []
    };

    if (Object.keys(validations).length) {
      Object.keys(validations).forEach(function (validationMethod) {

        if (validationRules[validationMethod] && typeof validations[validationMethod] === 'function') {
          throw new Error('Formsy does not allow you to override default validations: ' + validationMethod);
        }

        if (!validationRules[validationMethod] && typeof validations[validationMethod] !== 'function') {
          throw new Error('Formsy does not have the validation rule: ' + validationMethod);
        }

        if (typeof validations[validationMethod] === 'function') {
          var validation = validations[validationMethod].call(component, currentValues, value);
          if (typeof validation === 'string') {
            results.errors.push(validation);
            results.failed.push(validationMethod);
          } else if (!validation) {
            results.failed.push(validationMethod);
          }
          return;

        } else if (typeof validations[validationMethod] !== 'function') {
          var validation = validationRules[validationMethod].call(component, currentValues, value, validations[validationMethod]);
          if (typeof validation === 'string') {
            results.errors.push(validation);
            results.failed.push(validationMethod);
          } else if (!validation) {
            results.failed.push(validationMethod);
          } else {
            results.success.push(validationMethod);
          }
          return;

        }

        return results.success.push(validationMethod);

      });
    }
    return results;

  }

  validateForm () {

    var onValidationComplete = function () {
      var originValid = this.state.isValid;
      var allIsValid = this.inputs.every(component => {
        return component.state._isValid;
      });

      this.setState({
        isValid: allIsValid
      }, originValid !== allIsValid ? this.props.validateComponent : null);

      if (allIsValid) {
        this.props.onValid();
      } else {
        this.props.onInvalid();
      }

      this.setState({
        canChange: true
      });

    }.bind(this);

    this.inputs.forEach((component, index) => {
      var validation = this.runValidation(component);
      if (validation.isValid && component.state._externalError) {
        validation.isValid = false;
      }
      component.setState({
        _isValid: validation.isValid,
        _isRequired: validation.isRequired,
        _validationError: validation.error,
        _externalError: !validation.isValid && component.state._externalError ? component.state._externalError : null
      }, index === this.inputs.length - 1 ? onValidationComplete : null);
    });

    // If there are no inputs, set state where form is ready to trigger
    // change event. New inputs might be added later
    if (!this.inputs.length && this.isMounted()) {
      this.setState({
        canChange: true
      });
    }
  }

}

/**
 * 让 tr 可拖拽
 */
const cardTarget = {
  canDrop() {
    return true;
  },

  hover(props, monitor, component) {
    const { id: draggedId, originalIndex: dragIndex} = monitor.getItem();
    const { id: overId } = props;

    if (draggedId !== overId) {
      const { index: overIndex } = props.findRow(overId);

      const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
      const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
      const clientOffset = monitor.getClientOffset();
      const hoverClientY = clientOffset.y - hoverBoundingRect.top;


      // Dragging downwards
      if (dragIndex < overIndex && hoverClientY < hoverMiddleY) {
        return;
      }

      // Dragging upwards
      if (dragIndex > overIndex && hoverClientY > hoverMiddleY) {
        return;
      }

      props.moveRow(draggedId, overIndex);
    }
  }
};
const cardSource = {
  beginDrag(props) {
    return {
      id: props.id,
      originalIndex: props.findRow(props.id).index
    };
  },

  endDrag(props, monitor) {
    const { id: droppedId, originalIndex } = monitor.getItem();
    const didDrop = monitor.didDrop();

    if (didDrop) {
      props.moveRowConfirm(droppedId);
    } else {
      props.moveRow(droppedId, originalIndex, true);
    }
  }
};
@DropTarget(CARD_TYPE, cardTarget, connect => ({
  connectDropTarget: connect.dropTarget()
}))
@DragSource(CARD_TYPE, cardSource, (connect, monitor) => ({
  connectDragSource: connect.dragSource(),
  connectDragPreview: connect.dragPreview(),
  isDragging: monitor.isDragging()
}))
class DraggableItem extends PureComponent {
  static propTypes = {
    connectDragSource: PropTypes.func.isRequired,
    connectDragPreview: PropTypes.func.isRequired,
    connectDropTarget: PropTypes.func.isRequired,
    id: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.string
    ]).isRequired,
    isDragging: PropTypes.bool.isRequired,
    findRow: PropTypes.func.isRequired,
    moveRow: PropTypes.func.isRequired,
    moveRowConfirm: PropTypes.func.isRequired
  };

  render() {
    const {
      isDragging,
      connectDragSource,
      connectDragPreview,
      connectDropTarget,
      className
    } = this.props;

    // console.log('Re render');

    return connectDropTarget(connectDragPreview(
      <div
        className={cx(className, {
          'tr-dragging': isDragging
        })}
      >
        {React.Children.map(this.props.children, child => {
          child = React.cloneElement(child, assign({
            ref: (component) => {
              if(!component) {
                return;
              }
              const dom = findDOMNode(component).querySelector('.drag-bar');
              connectDragSource(dom);
            }
          }, child.props));

          return child;
        })}
      </div>
    ));
  }
}


@Renderer({
  name: 'form-combo-form',
  desc: '...',
  test: /(?:^|\/)combo-form$/
})
export default class FormItemComboForm extends ComboForm {}
